
Chapter 14  Installable Device Drivers

  Device drivers are the modules of an operating system that control the
  hardware. They isolate the operating-system kernel from the specific
  characteristics and idiosyncrasies of the peripheral devices interfaced to
  the central processor. Thus, the driver's relationship to the kernel is
  analogous to the operating system's relationship to application programs.

  The installable device drivers that were introduced in MS-DOS version 2
  give the user great flexibility. They allow the user to customize and
  configure the computer for a wide range of peripheral devices, with a
  minimum of troublesome interactions and without having to "patch" the
  operating system. Even the most inexperienced user can install a new
  device into a system by plugging in a card, copying a driver file to the
  boot disk, and editing the system configuration file.

  For those inclined to do their own programming, the MS-DOS installable
  device drivers are interfaced to the hardware-independent kernel through a
  simple and clearly defined scheme of function codes and data structures.
  Given adequate information about the hardware, any competent assembly-
  language programmer can expect to successfully interface even the most
  bizarre device to MS-DOS without altering the operating system in the
  slightest and without acquiring any special or proprietary knowledge about
  its innards.

  In retrospect, installable device drivers have proven to be one of the key
  usability features of MS-DOS. I feel that they have been largely
  responsible for the rapid proliferation and competitive pricing of
  high-speed mass-storage devices for MS-DOS machines, and for the growing
  confidence of the average user toward "tampering with" (upgrading) his or
  her machine.


MS-DOS Device-Driver Types

  Drivers written for MS-DOS fall into two distinct classes:

    Block-device drivers

    Character-device drivers

  A driver's class determines what functions it must support, how it is
  viewed by MS-DOS, and how it makes the associated physical device appear
  to behave when an application program makes a request for I/O.

Character-Device Drivers

  Character-device drivers control peripheral devices that perform input and
  output one character (or byte) at a time, such as a terminal or printer. A
  single character-device driver ordinarily supports a single hardware unit.
  Each character device has a one-to-eight-character logical name, and an
  application program can use this name to open the device for input or
  output, as though it were a file. The logical name is strictly a means of
  identification for MS-DOS and has no physical equivalent on the device.

  MS-DOS's built-in character-device drivers for the console, serial port,
  and printer are unique in that an application program can access them in
  three different ways:

    It can open them by name (CON, AUX, PRN, etc.) for input and output,
     like any other character device.

    It can use the special-purpose MS-DOS function calls (Int 21H Functions
     01-0CH).

    It can use the default handles (standard input, standard output,
     standard error, standard auxiliary, and standard printer), which do not
     need to be opened to be used.

  The number of additional character-device drivers that can be installed is
  limited only by available memory and by the requirement that each driver
  have a unique logical name. If more than one driver uses the same logical
  name, the last driver to be loaded will supersede any others and will
  receive all I/O requests addressed to that logical name. This fact can
  occasionally be turned to advantage; for example, it allows the user to
  replace the system's default CON driver, which does not support cursor
  positioning or character attributes, with the more powerful ANSI.SYS
  driver.

  ASCII vs Binary Mode

  MS-DOS regards a handle associated with a character device to be in either
  ASCII (cooked) mode or binary (raw) mode. The mode affects MS-DOS's
  buffering of data for read and write requests. The driver itself is not
  aware of the mode, and the mode does not affect its operation. An
  application can select the mode of a handle with the IOCTL function (Int
  21H Function 44H).

  During ASCII-mode input, MS-DOS requests characters one at a time from the
  driver and places them into its own internal buffer, echoing each to the
  screen (if the input device is the keyboard) and checking each character
  for a Ctrl-C (03H). When the number of characters requested by the
  application program has been received, when a Ctrl-Z is detected, or when
  the Enter key is pressed (in the case of the keyboard), MS-DOS terminates
  the input and copies the data from its internal buffer into the requesting
  program's buffer. Similarly, during ASCII-mode output, MS-DOS passes the
  characters to the device driver one at a time and checks for a Ctrl-C
  pending at the keyboard between each character. When a Ctrl-C is detected,
  MS-DOS aborts the input or output operation and transfers to the routine
  whose address is stored in the Int 23H vector.

  In binary mode, MS-DOS reads or writes the exact number of bytes requested
  by the application program, without regard to any control characters such
  as Enter or Ctrl-C. MS-DOS passes the entire request through to the driver
  in a single operation, instead of breaking it into single-character reads
  or writes, and transfers the characters directly to or from the requesting
  program's buffer.

Block-Device drivers

  Block-device drivers usually control random-access mass-storage devices
  such as floppy-disk drives and fixed disks, although they can also be used
  to control non-random-access devices such as magnetic-tape drives. Block
  devices transfer data in chunks, rather than one byte at a time. The size
  of the blocks may be either fixed (disk drives) or variable (tape drives).

  A block driver can support more than one hardware unit, map a single
  physical unit onto two or more logical units, or both. Block devices do
  not have file-like logical names, as character devices do. Instead, MS-DOS
  assigns drive designators to the block-device units or logical drives in
  an alphabetic sequence: A, B, and so forth. Each logical drive contains a
  file system: boot block, file allocation table, root directory, and so
  forth. (See Chapter 10.)

  A block-device driver's position in the chain of all drivers determines
  the first letter assigned to that driver. The number of logical drive
  units that the driver supports determines the total number of letters
  assigned to it.

  Block-device drivers always read or write exactly the number of sectors
  requested (barring hardware or addressing errors) and never filter or
  otherwise manipulate the contents of the blocks being transferred.


Structure of an MS-DOS Device Driver

  A device driver consists of three major parts (Figure 14-1):

    A device header

    A strategy (strat) routine

    An interrupt (intr) routine

  We'll discuss each of these in more detail as we work through this
  chapter.

  +++
  |                        |     Initialization      |
  |                        ++
  |                        |       Media check       |
  |                        ++
  |                        |        Build BPB        |
  |                        ++
  |                        |  IOCTL read and write   |
  |                        ++
  |                        |         Status          |
  |                        ++
  |                        |          Read           |
  |                        ++
  |                        |   Write, write/verify   |
  |                        ++
  |   Interrupt routine    |    Output until busy    |
  |                        ++
  |                        |      Flush buffers      |
  |                        ++
  |                        |       Device open       |
  |                        ++
  |                        |      Device close       |
  |                        ++
  |                        | Check whether removable |
  |                        ++
  |                        |     Generic IOCTL       |
  |                        ++
  |                        | Get/Set logical device  |
  |                        ++
  ++
  |                 Strategy routine                 |
  ++
  |               Device-driver header               |
  ++

  Figure 14-1.  General structure of an MS-DOS installable device driver.

The Device Header

  The device header (Figure 14-2) lies at the beginning of the driver. It
  contains a link to the next driver in the chain, a set of attribute flags
  for the device (Figure 14-3), offsets to the executable strategy and
  interrupt routines for the device, and the logical-device name (if it is a
  character device such as PRN or COM1) or the number of logical units (if
  it is a block device).

  Byte offset

  00H ++
      |         Link to next driver, offset          |
  02H ++
      |         Link to next driver, segment         |
  04H ++
      |            Device attribute word             |
  06H ++
      |         Strategy entry point, offset         |
  08H ++
      |        Interrupt entry point, offset         |
  0AH ++
      |  Logical name (8 bytes) if character device  |
      |  Number of units (1 byte) if block device,   |
      |    followed by 7 bytes of reserved space     |
      ++

  Figure 14-2.  Device-driver header. The offsets to the strat and intr
  routines are offsets from the same segment used to point to the device
  header.


  Bit            Significance
  
  15             1 if character device, 0 if block device
  14             1 if IOCTL read and write supported
  13             for block devices:
                 1 if BIOS parameter block in boot sector should be used to
                 determine media characteristics, 0 if media ID byte should
                 be used
                 for character devices:
                 1 if output until busy supported
  12             Reserved (should be 0)
  11             1 if open/close/removable media supported (MS-DOS 3.0 and
                 later)
  710           Reserved (should be 0)
  6              1 if generic IOCTL and get/set logical drive supported
                 (MS-DOS 3.2 and later)
  5              Reserved (should be 0)
  4              1 if CON driver and Int 29H fast-output function supported
  3              1 if current CLOCK$ device
  2              1 if current NUL device
  1              for block devices:
                 1 if driver supports 32-bit sector addressing (MS-DOS 4.0)
                 for character devices:
                 1 if standard output device (stdout)
  0              1 if current standard input device (stdin)
  


  Figure 14-3.  Device attribute word in device header. In block-device
  drivers, only bits 6, 11, and 1315 (and bit 1 in MS-DOS version 4.0) have
  significance; the remainder should always be zero.

The Strategy Routine

  MS-DOS calls the strategy routine (strat) for the device when the driver
  is first loaded and installed, and again whenever an application program
  issues an I/O request for the device. MS-DOS passes the strategy routine a
  double-word pointer to a data structure called a request header. This
  structure contains information about the type of operation to be
  performed. In current versions of MS-DOS, the strategy routine never
  actually performs any I/O operation but simply saves the pointer to the
  request header. The strat routine must not make any Int 21H function
  calls.

  The first 13 bytes of the request header are the same for all
  device-driver functions and are therefore referred to as the static
  portion of the header. The number and contents of the subsequent bytes
  vary according to the type of function being requested (Figure 14-4).
  Both MS-DOS and the driver read and write information in the request
  header.

  The request header's most important component is a command code, or
  function number, passed in its third byte to select a driver subfunction
  such as read, write, or status. Other information passed to the driver in
  the header includes unit numbers, transfer addresses, and sector or byte
  counts.

  
  ;
  ; MS-DOS request header structure definition
  ;
  Request         struc                  ; request header template structure

  Rlength         db    ?                ; 0  length of request header
  Unit            db    ?                ; 1  unit number for this request
  Command         db    ?                ; 2  request header's command code
  Status          dw    ?                ; 3  driver's return status word
  Reserve         db    8 dup (?)        ; 5  reserved area
  Media           db    ?                ; 13 media descriptor byte
  Address         dd    ?                ; 14 memory address for transfer
  Count           dw    ?                ; 18 byte/sector count value
  Sector          dw    ?                ; 20 starting sector value

  Request         ends                   ; end of request header template
  

  Figure 14-4.  Format of request header. Only the first 13 bytes are common
  to all driver functions; the number and definition of the subsequent bytes
  vary, depending upon the function type. The structure shown here is the
  one used by the read and write subfunctions of the driver.

The Interrupt Routine

  The last and most complex part of a device driver is the interrupt routine
  (intr), which MS-DOS calls immediately after it calls the strategy
  routine. The interrupt routine implements the device driver proper; it
  performs (or calls other resident routines to perform) the actual input or
  output operations, based on the information passed in the request header.
  The strat routine may not make any Int 21H function calls, except for a
  restricted set during driver initialization.

  When an I/O function is completed, the interrupt routine uses the status
  field in the request header to inform the DOS kernel about the outcome of
  the requested I/O operation. It can use other fields in the request header
  to pass back such useful information as counts of the actual sectors or
  bytes transferred.

  The interrupt routine usually consists of the following elements:

    A collection of subroutines to implement the various function types
     that may be requested by MS-DOS (sometimes called the command-code
     routines)

    A centralized entry point that saves all affected registers, extracts
     the desired function code from the request header, and branches to the
     appropriate command-code routine (typically accomplished with a jump
     table)

    A centralized exit point that stores status and error codes into the
     request header (Figures 14-5 and 14-6) and restores the previous
     contents of the affected registers

  The command-code routines that implement the various functions supported
  by an installable device driver are discussed in detail in the following
  pages.

  Bit(s)         Significance
  
  15             Error
  1214          Reserved
  9              Busy
  8              Done
  07            Error code if bit 15 = 1
  

  Figure 14-5.  Values for the return status word of the request header.

  Code           Meaning
  
  0              Write-protect violation
  1              Unknown unit
  2              Drive not ready
  3              Unknown command
  4              Data error (CRC)
  5              Bad request-structure length
  6              Seek error
  7              Unknown medium
  8              Sector not found
  9              Printer out of paper
  0AH            Write fault
  0BH            Read fault
  0CH            General failure
  0D0EH         Reserved
  0FH            Invalid disk change (MS-DOS versions 3.0 and later)
  

  Figure 14-6.  Driver error codes returned in bits 0 through 7 of the
  return status word of the request header.

  Although its name suggests otherwise, the interrupt routine is never
  entered asynchronously (on an I/O completion interrupt, for example).
  Thus, the division of function between strategy and interrupt routines is
  completely artificial in the current versions of MS-DOS.


The Command-Code Routines

  A total of 20 command codes are defined for MS-DOS device drivers. The
  command codes (which are not consecutive), the names of the associated
  driver-interrupt routines, and the MS-DOS versions in which they are first
  supported are as follows:


  Command     Function                 Character   Block        MS-DOS
  code                                 driver      driver       version
  
  0           Init (Initialization)    X           X            2.0
  1           Media Check                          X            2.0
  2           Build BPB                            X            2.0
  3           IOCTL Read               X           X            2.0
  4           Read                     X           X            2.0
  5           Nondestructive Read      X                        2.0
  6           Input Status             X                        2.0
  7           Flush Input Buffers      X                        2.0
  8           Write                    X           X            2.0
  9           Write with Verify                    X            2.0
  10          Output Status            X                        2.0
  11          Flush Output Buffers     X                        2.0
  12          IOCTL Write              X           X            2.0
  13          Device Open              X           X            3.0
  14          Device Close             X           X            3.0
  15          Removable Media                      X            3.0
  16          Output Until Busy        X                        3.0
  19          Generic IOCTL            X           X            3.2
  23          Get Logical Device                   X            3.2
  24          Set Logical Device                   X            3.2
  


  As you can see from the preceding table, a driver's interrupt section must
  support functions 0 through 12 under all versions of MS-DOS. Drivers
  tailored for MS-DOS 3.0 and 3.1 can optionally support an additional four
  functions, and MS-DOS drivers for versions 3.2 and later can support three
  more (for a total of 20). MS-DOS inspects the bits in the attribute word
  of the device-driver header to determine which of the optional functions a
  driver supports, if any.

  Some of the functions are relevant only for character-device drivers and
  some only for block-device drivers; a few have meaning to both types. In
  any case, both driver types should have an executable routine present for
  each function, even if it does nothing except set the done flag in the
  status word of the request header.

  In the command-code descriptions that follow, RH refers to the request
  header whose address was passed to the strategy routine in ES:BX, BYTE is
  an 8-bit parameter, WORD is a 16-bit parameter, and DWORD is a far pointer
  (a 16-bit offset followed by a 16-bit segment).

Function 00H (0): Driver Initialization

  MS-DOS requests the driver's initialization function (init) only once,
  when the driver is first loaded. This function performs any necessary
  device hardware initialization, setup of interrupt vectors, and so forth.
  The initialization routine must return the address of the position where
  free memory begins after the driver code (the break address), so that
  MS-DOS knows where it can build certain control structures and then load
  the next installable driver. If this is a block-device driver, init must
  also return the number of units and the address of a BPB pointer array.

  MS-DOS uses the number of units returned by a block driver in the request
  header to assign drive identifiers. For example, if the current maximum
  drive is D and the driver being initialized supports four units, MS-DOS
  will assign it the drive letters E, F, G, and H. Although the
  device-driver header also has a field for number of units, MS-DOS does not
  inspect it.

  The BPB pointer array is an array of word offsets to BIOS parameter blocks
  (Figure 14-7). Each unit defined by the driver must have one entry in the
  array, although the entries can all point to the same BPB to conserve
  memory. During the operating-system boot sequence, MS-DOS scans all the
  BPBs defined by all the units in all the block-device drivers to determine
  the largest sector size that exists on any device in the system and uses
  this information to set its cache buffer size.

  The operating-system services that the initialization code can invoke at
  load time are very limited only Int 21H Functions 01H through 0CH and
  30H. These are just adequate to check the MS-DOS version number and
  display a driver-identification or error message.

  Many programmers position the initialization code at the end of the driver
  and return that address as the location of the first free memory, so that
  MS-DOS will reclaim the memory occupied by the initialization routine
  after the routine is finished with its work. If the initialization routine
  finds that the device is missing or defective and wants to abort the
  installation of the driver completely so that it does not occupy any
  memory, it should return number of units as zero and set the free memory
  address to CS:0000H. (A character-device driver that wants to abort its
  installation should clear bit 15 of the attribute word in the driver
  header and then set the units field and free memory address as though it
  were a block-device driver.)

  Byte(s)                  Contents
  
  0001H                   Bytes per sector
  02H                      Sectors per allocation unit (power of 2)
  03H04H                  Number of reserved sectors (starting at sector 0)
  05H                      Number of file allocation tables
  06H07H                  Maximum number of root-directory entries
  08H09H                  Total number of sectors in medium
  0AH                      Media descriptor byte
  0BH0CH                  Number of sectors occupied by a single FAT
  0DH0EH                  Sectors per track (versions 3.0 and later)
  0FH10H                  Number of heads (versions 3.0 and later)
  11H12H                  Number of hidden sectors (versions 3.0 and later)
  13H14H                  High-order word of number of hidden sectors
                           (version 4.0)
  15H18H                  If bytes 89 are zero, total number of sectors in
                           medium (version 4.0)
  19H1EH                  Reserved, should be zero (version 4.0)
  

  Figure 14-7.  Structure of a BIOS parameter block (BPB). Every formatted
  disk contains a copy of its BPB in the boot sector. (See Chapter 10.)

  The initialization function is called with

  
  RH + 2             BYTE              Command code = 0

  RH + 18            DWORD             Pointer to character after equal sign
                                       on CONFIG.SYS line that loaded driver
                                       (this information is read-only)

  RH + 22            BYTE              Drive number for first unit of this
                                       block driver (0 = A, 1 = B, and so
                                       forth) (MS-DOS version 3 only)
  

  It returns:

  
  RH + 3             WORD              Status

  RH + 13            BYTE              Number of units (block devices only)

  RH + 14            DWORD             Address of first free memory above
                                       driver (break address)

  RH + 18            DWORD             BPB pointer array (block devices
                                       only)
  

Function 01H (1): Media Check

  The media-check function applies only to block devices, and in
  character-device drivers it should do nothing except set the done flag.
  This function is called when a drive-access call other than a simple file
  read or write is pending. MS-DOS passes to the function the media
  descriptor byte for the disk that it assumes is in the drive (Figure
  14-8). If feasible, the media-check routine returns a code indicating
  whether the disk has been changed since the last transfer. If the
  media-check routine can assert that the disk has not been changed, MS-DOS
  can bypass rereading the FAT before a directory access, which improves
  overall performance.

  Code                     Meaning
  
  0F0H                     3.5", 2-sided, 18-sector
  0F8H                     fixed disk
  0F9H                     3.5", 2-sided, 9-sector
  0F9H                     5.25", 2-sided, 15-sector
  0FCH                     5.25", 1-sided, 9-sector
  0FDH                     5.25", 2-sided, 9-sector
  0FEH                     5.25", 1-sided, 8-sector
  0FFH                     5.25", 2-sided, 8-sector
  

  Figure 14-8.  Current valid MS-DOS codes for the media descriptor byte of
  the request header, assuming bit 13 in the attribute word of the driver
  header is zero.

  MS-DOS responds to the results of the media-check function in the
  following ways:

    If the disk has not been changed, MS-DOS proceeds with the disk access.

    If the disk has been changed, MS-DOS invalidates all buffers associated
     with this unit, including buffers containing data waiting to be written
     (this data is simply lost), performs a BUILD BPB call, and then reads
     the disk's FAT and directory.

    If the disk-change status is unknown, the action taken by MS-DOS
     depends upon the state of its internal buffers. If data that needs to
     be written out is present in the buffers, MS-DOS assumes no disk change
     has occurred and writes the data (taking the risk that, if the disk
     really was changed, the file structure on the new disk may be damaged).
     If the buffers are empty or have all been previously flushed to the
     disk, MS-DOS assumes that the disk was changed, and then proceeds as
     described above for the disk-changed return code.

  If bit 11 of the device-header attribute word is set (that is, the driver
  supports the optional open/close/removable-media functions), the host
  system is MS-DOS version 3.0 or later, and the function returns the
  disk-changed code (-1), the function must also return the segment and
  offset of the ASCIIZ volume label for the previous disk in the drive. (If
  the driver does not have the volume label, it can return a pointer to the
  ASCIIZ string NO NAME.) If MS-DOS determines that the disk was changed
  with unwritten data still present in its buffers, it issues a
  critical-error 0FH (invalid disk change). Application programs can trap
  this critical error and prompt the user to replace the original disk.

  The media-check function is called with

  
  RH + 1             BYTE              Unit code

  RH + 2             BYTE              Command code = 1

  RH + 13            BYTE              Media descriptor byte
  

  It returns

  
  RH + 3             WORD              Status

  RH + 14            BYTE              Media-change code:

                                       -1 if disk changed

                                       0 if don't know whether disk changed

                                       1 if disk not changed

  RH + 15            DWORD             Pointer to previous volume label, if
                                       device attribute bit 11 = 1 and disk
                                       has been changed (MS-DOS versions 3.0
                                       and later)
  

Function 02H (2): Build BIOS Parameter Block (BPB)

  The build BPB function applies only to block devices, and in
  character-device drivers should do nothing except set the done flag. The
  kernel uses this function to get a pointer to the valid BPB (see Figure
  14-7) for the current disk and calls it when the disk-changed code is
  returned by the media-check routine or the don't-know code is returned and
  there are no dirty buffers (buffers with changed data that have not yet
  been written to disk). Thus, a call to this function indicates that the
  disk has been legally changed.

  The build BPB function receives a pointer to a one-sector buffer in the
  request header. If bit 13 in the driver header's attribute word is zero,
  the buffer contains the first sector of the FAT (which includes the media
  identification byte) and should not be altered by the driver. If bit 13 is
  set, the driver can use the buffer as scratch space.

  The build BPB function is called with

  
  RH + 1             BYTE              Unit code

  RH + 2             BYTE              Command code = 2

  RH + 13            BYTE              Media descriptor byte

  RH + 14            DWORD             Buffer address
  

  It returns

  
  RH + 3             WORD              Status

  RH + 18            DWORD             Pointer to new BPB
  

  Under MS-DOS versions 3.0 and later, if bit 11 of the header's device
  attribute word is set, this routine should also read the volume label off
  the disk and save it.

Function 03H (3): I/O-Control Read

  The IOCTL read function allows the device driver to pass information
  directly to the application program. This function is called only if bit
  14 is set in the device attribute word. MS-DOS performs no error check on
  IOCTL I/O calls.

  The IOCTL read function is called with

  
  RH + 1             BYTE              Unit code (block devices)

  RH + 2             BYTE              Command code = 3

  RH + 13            BYTE              Media descriptor byte

  RH + 14            DWORD             Transfer address

  RH + 18            WORD              Byte/sector count

  RH + 20            WORD              Starting sector number (block
                                       devices)
  

  It returns

  
  RH + 3             WORD              Status

  RH + 18            WORD              Actual bytes or sectors transferred
  

Function 04H (4): Read

  The read function transfers data from the device into the specified memory
  buffer. If an error is encountered during the read, the function must set
  the error status and, in addition, report the number of bytes or sectors
  successfully transferred; it is not sufficient to simply report an error.

  The read function is called with

  
  RH + 1             BYTE              Unit code (block devices)

  RH + 2             BYTE              Command code = 4

  RH + 13            BYTE              Media descriptor byte

  RH + 14            DWORD             Transfer address

  RH + 18            WORD              Byte/sector count

  RH + 20            WORD              Starting sector number (block
                                       devices)
  

  For block-device read operations in MS-DOS version 4, if the logical unit
  is larger than 32 MB and bit 1 of the driver's attribute word is set, the
  following request structure is used instead:

  
  RH + 1             BYTE              Unit code

  RH + 2             BYTE              Command code = 4

  RH + 13            BYTE              Media descriptor byte

  RH + 14            DWORD             Transfer address

  RH + 18            WORD              Sector count

  RH + 20            WORD              Contains -1 to signal use of 32-bit
                                       sector number

  RH + 26            DWORD             32-bit starting sector number
  

  The read function returns

  
  RH + 3             WORD              Status

  RH + 18            WORD              Actual bytes or sectors transferred

  RH + 22            DWORD             Pointer to volume label if error 0FH
                                       is returned (MS-DOS versions 3.0 and
                                       later)
  

  Under MS-DOS versions 3.0 and later, this routine can use the count of
  open files maintained by the open and close functions (0DH and 0EH) and
  the media descriptor byte to determine whether the disk has been illegally
  changed.

Function 05H (5): Nondestructive Read

  The nondestructive read function applies only to character devices, and in
  block devices it should do nothing except set the done flag. It returns
  the next character that would be obtained with a read function (command
  code 4), without removing that character from the driver's internal
  buffer. MS-DOS uses this function to check the console driver for pending
  Control-C characters during other operations.

  The nondestructive read function is called with

  
  RH + 2             BYTE              Command code = 5
  

  It returns

  
  RH + 3             WORD              Status

                                       If busy bit = 0, at least one
                                       character is waiting

                                       If busy bit = 1, no characters are
                                       waiting

  RH + 13            BYTE              Character (if busy bit = 0)
  

Function 06H (6): Input Status

  The input-status function applies only to character devices, and in
  block-device drivers it should do nothing except set the done flag. This
  function returns the current input status for the device, allowing MS-DOS
  to test whether characters are waiting in a type-ahead buffer. If the
  character device does not have a type-ahead buffer, the input-status
  routine should always return the busy bit equal to zero, so that MS-DOS
  will not wait forever to call the read (04H) or nondestructive read (05H)
  function.

  The input-status function is called with

  
  RH + 2             BYTE              Command code = 6
  

  It returns

  
  RH + 3             WORD              Status:

                                       If busy bit = 1, read request goes to
                                       physical device.

                                       If busy bit = 0, characters already
                                       in device buffer and read request
                                       returns quickly.
  

Function 07H (7): Flush Input Buffers

  The flush-input-buffers function applies only to character devices, and in
  block-device drivers it should do nothing except set the done flag. This
  function causes any data waiting in the input buffer to be discarded.

  The flush-input-buffers function is called with

  
  RH + 2             BYTE              Command code = 7
  

  It returns

  
  RH + 3             WORD              Status

  

Function 08H (8): Write

  The write function transfers data from the specified memory buffer to the
  device. If an error is encountered during the write, the write function
  must set the error status and, in addition, report the number of bytes or
  sectors successfully transferred; it is not sufficient to simply report an
  error.

  The write function is called with

  
  RH + 1             BYTE              Unit code (block devices)

  RH + 2             BYTE              Command code = 8

  RH + 13            BYTE              Media descriptor byte

  RH + 14            DWORD             Transfer address

  RH + 18            WORD              Byte/sector count

  RH + 20            WORD              Starting sector number (block
                                       devices)
  

  For block-device write operations in MS-DOS version 4, if the logical unit
  is larger than 32 MB and bit 1 of the driver's attribute word is set, the
  following request structure is used instead:

  
  RH + 1             BYTE              Unit code

  RH + 2             BYTE              Command code = 8

  RH + 13            BYTE              Media descriptor byte

  RH + 14            DWORD             Transfer address

  RH + 18            WORD              Sector count

  RH + 20            WORD              Contains -1 to signal use of 32-bit
                                       sector number

  RH + 26            DWORD             32-bit starting sector number
  

  The write function returns

  
  RH + 3             WORD              Status

  RH + 18            WORD              Actual bytes or sectors transferred

  RH + 22            DWORD             Pointer to volume label if error 0FH
                                       returned (MS-DOS versions 3.0 and
                                       later)
  

  Under MS-DOS versions 3.0 and later, this routine can use the reference
  count of open files maintained by the open and close functions (0DH and
  0EH) and the media descriptor byte to determine whether the disk has been
  illegally changed.

Function 09H (9): Write with Verify

  The write-with-verify function transfers data from the specified memory
  buffer to the device. If feasible, it should perform a read-after-write
  verification of the data to confirm that the data was written correctly.
  Otherwise, Function 09H is exactly like Function 08H.

Function 0AH (10): Output Status

  The output-status function applies only to character devices, and in
  block-device drivers it should do nothing except set the done flag. This
  function returns the current output status for the device.

  The output-status function is called with

  
  RH + 2             BYTE              Command code = 10 (0AH)
  

  It returns

  
  RH + 3             WORD              Status:

                                       If busy bit = 1, write request waits
                                       for completion of current request.

                                       If busy bit = 0, device idle and
                                       write request starts immediately.
  

Function 0BH (11): Flush Output Buffers

  The flush-output-buffers function applies only to character devices, and
  in block-device drivers it should do nothing except set the done flag.
  This function empties the output buffer, if any, and discards any pending
  output requests.

  The flush-output-buffers function is called with

  
  RH + 2             BYTE              Command code = 11 (0BH)
  

  It returns

  
  RH + 3             WORD              Status

  

Function 0CH (12): I/O-Control Write

  The IOCTL write function allows an application program to pass control
  information directly to the driver. This function is called only if bit 14
  is set in the device attribute word. MS-DOS performs no error check on
  IOCTL I/O calls.

  The IOCTL write function is called with

  
  RH + 1             BYTE              Unit code (block devices)

  RH + 2             BYTE              Command code = 12 (0CH)

  RH + 13            BYTE              Media descriptor byte

  RH + 14            DWORD             Transfer address

  RH + 18            WORD              Byte/sector count

  RH + 20            WORD              Starting sector number (block
                                       devices)
  

  It returns

  
  RH + 3             WORD              Status

  RH + 18            WORD              Actual bytes or sectors transferred
  

Function 0DH (13): Device Open

  The device-open function is supported only under MS-DOS versions 3.0 and
  later and is called only if bit 11 is set in the device attribute word of
  the device header.

  On block devices, the device-open function can be used to manage local
  buffering and to increment a reference count of the number of open files
  on the device. This capability must be used with care, however, because
  programs that access files through FCBs frequently fail to close them,
  thus invalidating the open-files count. One way to protect against this
  possibility is to reset the open-files count to zero, without flushing the
  buffers, whenever the answer to a media-change call is yes and a
  subsequent build BPB call is made to the driver.

  On character devices, the device-open function can be used to send a
  device-initialization string (which can be set into the driver by an
  application program by means of an IOCTL write function) or to deny
  simultaneous access to a character device by more than one process. Note
  that the predefined handles for the CON, AUX, and PRN devices are always
  open.

  The device-open function is called with

  
  RH + 1             BYTE              Unit code (block devices)

  RH + 2             BYTE              Command code = 13 (0DH)
  

  It returns

  
  RH + 3             WORD              Status
  

Function 0EH (14): Device Close

  The device-close function is supported only under MS-DOS versions 3.0 and
  later and is called only if bit 11 is set in the device attribute word of
  the device header.

  On block devices, this function can be used to manage local buffering and
  to decrement a reference count of the number of open files on the device;
  when the count reaches zero, all files have been closed and the driver
  should flush buffers because the user may change disks.

  On character devices, the device-close function can be used to send a
  device-dependent post-I/O string such as a formfeed. (This string can be
  set into the driver by an application program by means of an IOCTL write
  function.) Note that the predefined handles for the CON, PRN, and AUX
  devices are never closed.

  The device-close function is called with

  
  RH + 1             BYTE              Unit code (block devices)

  RH + 2             BYTE              Command code = 14 (0EH)
  

  It returns

  
  RH + 3             WORD              Status
  

Function 0FH (15): Removable Media

  The removable-media function is supported only under MS-DOS versions 3.0
  and later and only on block devices; in character-device drivers it should
  do nothing except set the done flag. This function is called only if bit
  11 is set in the device attribute word in the device header.

  The removable-media function is called with

  
  RH + 1             BYTE              Unit code

  RH + 2             BYTE              Command code = 15 (0FH)
  

  It returns

  
  RH + 3             WORD              Status:

                                       If busy bit = 1, medium nonremovable

                                       If busy bit = 0, medium removable
  

Function 10H (16): Output Until Busy

  The output-until-busy function is supported only under MS-DOS versions 3.0
  and later, and only on character devices; in block-device drivers it
  should do nothing except set the done flag. This function transfers data
  from the specifi
d memory buffer to a device, continuing to transfer bytes
  until the device is busy. It is called only if bit 13 of the device
  attribute word is set in the device header.

  This function is an optimization included specifically for the use of
  print spoolers. It is not an error for this function to return a number of
  bytes transferred that is less than the number of bytes requested.

  The output-until-busy function is called with

  
  RH + 2             BYTE              Command code = 16 (10H)

  RH + 14            DWORD             Transfer address

  RH + 18            WORD              Byte count
  

  It returns

  
  RH + 3             WORD              Status

  RH + 18            WORD              Actual bytes transferred
  

Function 13H (19) Generic IOCTL

  The generic IOCTL function is supported only under MS-DOS versions 3.2 and
  later and is called only if bit 6 is set in the device attribute word of
  the device header. This function corresponds to the MS-DOS generic IOCTL
  service supplied to application programs by Int 21H Function 44H
  Subfunctions 0CH and 0DH.

  The generic IOCTL function is passed a category (major) code, a function
  (minor) code, the contents of the SI and DI registers at the point of the
  IOCTL call, and the segment and offset of a data buffer. This buffer in
  turn contains other information whose format depends on the major and
  minor IOCTL codes passed in the request header. The driver must interpret
  the major and minor codes in the request header and the contents of the
  additional buffer to determine which operation it will carry out, then set
  the done flag in the request-header status word, and return any other
  applicable information in the request header or the data buffer.

  Services that the generic IOCTL function may invoke, if the driver
  supports them, include configuration of the driver for nonstandard disk
  formats, reading and writing entire disk tracks of data, and formatting
  and verifying tracks. The generic IOCTL function has been designed to be
  open-ended, so that it can be used to easily extend the device-driver
  definition under future versions of MS-DOS.

  The generic IOCTL function is called with

  
  RH + 1             BYTE              Unit number (block devices)

  RH + 2             BYTE              Command code = 19 (13H)

  RH + 13            BYTE              Category (major) code

  RH + 14            BYTE              Function (minor) code

  RH + 15            WORD              SI register contents

  RH + 17            WORD              DI register contents

  RH + 19            DWORD             Address of generic IOCTL data packet
  

  It returns

  
  RH + 3             WORD              Status
  

Function 17H (23): Get Logical Device

  The get-logical-device function is supported only under MS-DOS versions
  3.2 and later and only on block devices; in character-device drivers it
  should do nothing except set the done bit in the status word. This
  function is called only if bit 6 is set in the device attribute word of
  the device header. It corresponds to the get-logical-device-map service
  supplied to application programs through Int 21H Function 44H Subfunction
  0EH.

  The get-logical-device function returns a code for the last drive letter
  used to reference the device; if only one drive letter is assigned to the
  device, the returned unit code should be zero. Thus, this function can be
  used to determine whether more than one drive letter is assigned to the
  same physical device.

  The get-logical-device function is called with

  
  RH + 1             BYTE              Unit code

  RH + 2             BYTE              Command code = 23 (17H)
  

  It returns

  
  RH + 1             BYTE              Last unit referenced, or zero

  RH + 3             WORD              Status
  

Function 18H (24): Set Logical Device

  The set-logical-device function is supported only under MS-DOS versions
  3.2 and later and only on block devices; in character-device drivers it
  should do nothing except set the done bit in the status word. This
  function is called only if bit 6 is set in the device attribute word of
  the device header. It corresponds to the set-logical-device-map service
  supplied to application programs by MS-DOS through Int 21H Function 44H
  Subfunction 0FH.

  The set-logical-device function informs the driver of the next
  logical-drive identifier that will be used to reference the physical
  device. The unit code passed by the MS-DOS kernel in this case is
  zero-based relative to the number of logical drives supported by this
  particular driver. For example, if the driver supports two floppy-disk
  units (A and B), only one physical floppy-disk drive exists in the system,
  and the set-logical-device function is called with a unit number of 1, the
  driver is being informed that the next read or write request from the
  kernel will be directed to drive B.

  The set-logical-device function is called with

  
  RH + 1             BYTE              Unit code

  RH + 2             BYTE              Command code = 24 (18H)
  

  It returns

  
  RH + 3             WORD              Status
  


The Processing of a Typical I/O Request

  An application program requests an I/O operation from MS-DOS by loading
  registers with the appropriate values and executing an Int 21H. This
  results in the following sequence of actions:

  1.  MS-DOS inspects its internal tables and determines which device driver
      should receive the I/O request.

  2.  MS-DOS creates a request-header data packet in a reserved area of
      memory. (Disk I/O requests are transformed from file and record
      information into logical-sector requests by MS-DOS's interpretation of
      the disk directory and FAT.)

  3.  MS-DOS calls the device driver's strat entry point, passing the
      address of the request header in the ES:BX registers.

  4.  The device driver saves the address of the request header in a local
      variable and performs a FAR RETURN.

  5.  MS-DOS calls the device driver's intr entry point.

  6.  The interrupt routine saves all registers, retrieves the address of
      the request header that was saved by the strategy routine, extracts
      the function code, and branches to the appropriate command-code
      subroutine to perform the function.

  7.  If a data transfer on a block device was requested, the driver's read
      or write subroutine translates the logical-sector number into a head,
      track, and physical-sector address for the requested unit and then
      performs the I/O operation. Because a multiple-sector transfer can be
      requested in a single request header, a single request by MS-DOS to
      the driver can result in multiple read or write commands to the disk
      controller.

  8.  When the requested function is complete, the interrupt routine sets
      the status word and any other required information into the request
      header, restores all registers to their state at entry, and performs a
      FAR RETURN.

  9.  MS-DOS translates the driver's return status into the appropriate
      return code and carry-flag status for the MS-DOS Int 21H function that
      was requested and returns control to the application program.

  Note that a single request by an application program can result in MS-DOS
  passing many request headers to the driver. For example, attempting to
  open a file in a subdirectory on a previously unaccessed disk drive might
  require the following actions:

    Reading the disk's boot sector to get the BPB

    Reading from one to many sectors of the root directory to find the
     entry for the subdirectory and obtain its starting-cluster number

    Reading from one to many sectors of both the FAT and the subdirectory
     itself to find the entry for the desired file


The CLOCK Driver: A Special Case

  MS-DOS uses the CLOCK device for marking file control blocks and directory
  entries with the date and time, as well as for providing the date and time
  services to application programs. This device has a unique type of
  interaction with MS-DOSa 6-byte sequence is read from or written to the
  driver that obtains or sets the current date and time. The sequence has
  the following format:

  +++++++
  |    0    |    1    |    2    |    3    |   4     |    5    |
  |  Days   |  Days   | Minutes |  Hours  |Seconds/ | Seconds |
  |low byte |high byte|         |         |  100    |         |
  +++++++

  The value passed for days is a 16-bit integer representing the number of
  days elapsed since January 1, 1980.

  The clock driver can have any logical-device name because MS-DOS uses the
  CLOCK bit in the device attribute word of the driver's device header to
  identify the device, rather than its name. On IBM PC systems, the clock
  device has the logical-device name CLOCK$.


Writing and Installing a Device Driver

  Now that we have discussed the structure and capabilities of installable
  device drivers for the MS-DOS environment, we can discuss the mechanical
  steps of assembling and linking them.

Assembly

  Device drivers for MS-DOS always have an origin of zero but are otherwise
  assembled, linked, and converted into an executable module as though they
  were .COM files. (Although MS-DOS is also capable of loading installable
  drivers in the .EXE file format, this introduces unnecessary complexity
  into writing and debugging drivers and offers no significant advantages.
  In addition, it is not possible to use .EXE-format drivers with some IBM
  versions of MS-DOS because the .EXE loader is located in COMMAND.COM,
  which is not present when the installable device drivers are being
  loaded.) The driver should not have a declared stack segment and must, in
  general, follow the other restrictions outlined in Chapter 3 for
  memory-image (.COM) programs. A driver can be loaded anywhere, so beware
  that you do not make any assumptions in your code about the driver's
  location in physical memory. Figure 14-9 presents a skeleton example that
  you can follow as you read the next few pages.

  
          name    driver
          page    55,132
          title   DRIVER.ASM Device-Driver Skeleton

  ;
  ; DRIVER.ASM   MS-DOS device-driver skeleton
  ;
  ; The driver command-code routines are stubs only and have
  ; no effect but to return a nonerror "done" status.
  ;
  ; Copyright 1988 Ray Duncan
  ;

  _TEXT   segment word public 'CODE'

          assume  cs:_TEXT,ds:_TEXT,es:NOTHING

          org     0

  MaxCmd  equ     24              ; maximum allowed command code:
                                  ; 12 for MS-DOS 2
                                  ; 16 for MS-DOS 3.0-3.1
                                  ; 24 for MS-DOS 3.2-3.3
  cr      equ     0dh             ; ASCII carriage return
  lf      equ     0ah             ; ASCII linefeed
  eom     equ     '$'             ; end-of-message signal


  Header:                         ; device-driver header
          dd      -1              ; link to next device driver
          dw      0c840h          ; device attribute word
          dw      Strat           ; "strategy" routine entry point
          dw      Intr            ; "interrupt" routine entry point
          db      'SKELETON'      ; logical-device name


  RHPtr   dd      ?               ; pointer to request header, passed
                                  ; by MS-DOS kernel to strategy routine


  Dispatch:                       ; interrupt-routine command-code
                                  ; dispatch table:
          dw      Init            ; 0  = initialize driver
          dw      MediaChk        ; 1  = media check
          dw      BuildBPB        ; 2  = build BPB
          dw      IoctlRd         ; 3  = IOCTL read
          dw      Read            ; 4  = read
          dw      NdRead          ; 5  = nondestructive read
          dw      InpStat         ; 6  = input status
          dw      InpFlush        ; 7  = flush input buffers
          dw      Write           ; 8  = write
          dw      WriteVfy        ; 9  = write with verify
          dw      OutStat         ; 10 = output status
          dw      OutFlush        ; 11 = flush output buffers
          dw      IoctlWt         ; 12 = IOCTL write
          dw      DevOpen         ; 13 = device open       (MS-DOS 3.0+)
          dw      DevClose        ; 14 = device close      (MS-DOS 3.0+)
          dw      RemMedia        ; 15 = removable media  (MS-DOS 3.0+)
          dw      OutBusy         ; 16 = output until busy (MS-DOS 3.0+)
          dw      Error           ; 17 = not used
          dw      Error           ; 18 = not used
          dw      GenIOCTL        ; 19 = generic IOCTL     (MS-DOS 3.2+)
          dw      Error           ; 20 = not used
          dw      Error           ; 21 = not used
          dw      Error           ; 22 = not used
          dw      GetLogDev       ; 23 = get logical device (MS-DOS 3.2+)
          dw      SetLogDev       ; 24 = set logical device (MS-DOS 3.2+)
  Strat   proc    far             ; device-driver strategy routine,
                                  ; called by MS-DOS kernel with
                                  ; ES:BX = address of request header

                                  ; save pointer to request header
          mov     word ptr cs:[RHPtr],bx
          mov     word ptr cs:[RHPtr+2],es

          ret                     ; back to MS-DOS kernel

  Strat   endp


  Intr    proc  far               ; device-driver interrupt routine,
                                  ; called by MS-DOS kernel immediately
                                  ; after call to strategy routine

          push    ax              ; save general registers
          push    bx
          push    cx
          push    dx
          push    ds
          push    es
          push    di
          push    si
          push    bp

          push    cs              ; make local data addressable
          pop     ds              ; by setting DS = CS

          les     di,[RHPtr]      ; let ES:DI = request header

                                  ; get BX = command code
          mov     bl,es:[di+2]
          xor     bh,bh
          cmp     bx,MaxCmd       ; make sure it's legal
          jle     Intr1           ; jump, function code is ok
          call    Error           ; set error bit, "unknown command" code
          jmp     Intr2

  Intr1:  shl     bx,1            ; form index to dispatch table
                                  ; and branch to command-code routine
          call    word ptr [bx+Dispatch]

          les     di,[RHPtr]      ; ES:DI = addr of request header

  Intr2:  or      ax,0100h        ; merge 'done' bit into status and
          mov     es:[di+3],ax    ; store status into request header

          pop     bp              ; restore general registers
          pop     si
          pop     di
          pop     es
          pop     ds
          pop     dx
          pop     cx
          pop     bx
          pop     ax
          ret                     ; back to MS-DOS kernel


  ; Command-code routines are called by the interrupt routine
  ; via the dispatch table with ES:DI pointing to the request
  ; header.  Each routine should return AX = 0 if function was
  ; completed successfully or AX = (8000h + error code) if
  ; function failed.


  MediaChk proc   near            ; function 1 = media check

          xor     ax,ax
          ret

  MediaChk endp


  BuildBPB proc   near            ; function 2 = build BPB

          xor     ax,ax
          ret

  BuildBPB endp


  IoctlRd proc    near            ; function 3 = IOCTL read

          xor     ax,ax
          ret

  IoctlRd endp
  Read    proc    near            ; function 4 = read (input)

          xor     ax,ax
          ret

  Read    endp


  NdRead  proc    near            ; function 5 = nondestructive read

          xor     ax,ax
          ret

  NdRead  endp


  InpStat proc    near            ; function 6 = input status

          xor     ax,ax
          ret

  InpStat endp


  InpFlush proc   near            ; function 7 = flush input buffers

          xor     ax,ax
          ret

  InpFlush endp


  Write   proc    near            ; function 8 = write (output)

          xor     ax,ax
          ret

  Write   endp


  WriteVfy proc   near            ; function 9 = write with verify

          xor     ax,ax
          ret
  endp


  OutStat proc    near            ; function 10 = output status

          xor     ax,ax
          ret

  OutStat endp


  OutFlush proc   near            ; function 11 = flush output buffers

          xor     ax,ax
          ret

  OutFlush endp


  IoctlWt proc    near            ; function 12 = IOCTL write

          xor     ax,ax
          ret

  IoctlWt endp


  DevOpen proc    near            ; function 13 = device open

          xor     ax,ax
          ret

  DevOpen endp


  DevClose proc   near            ; function 14 = device close

          xor     ax,ax
          ret

  DevClose endp
  RemMedia proc   near            ; function 15 = removable media

          xor     ax,ax
          ret

  RemMedia endp


  OutBusy proc    near            ; function 16 = output until busy

          xor     ax,ax
          ret

  OutBusy endp


  GenIOCTL proc   near            ; function 19 = generic IOCTL

          xor     ax,ax
          ret

  GenIOCTL endp


  GetLogDev proc  near            ; function 23 = get logical device

          xor     ax,ax
          ret

  GetLogDev endp


  SetLogDev proc  near            ; function 24 = set logical device

          xor     ax,ax
          ret

  SetLogDev endp


  Error   proc    near            ; bad command code in request header

          mov     ax,8003h        ; error bit + "unknown command" code
          ret
    endp


  Init    proc    near            ; function 0 = initialize driver

          push    es              ; save address of request header
          push    di

          mov     ax,cs           ; convert load address to ASCII
          mov     bx,offset Ident1
          call    hexasc

          mov     ah,9            ; display driver sign-on message
          mov     dx,offset Ident
          int     21h

          pop     di              ; restore request-header address
          pop     es

                                  ; set address of free memory
                                  ; above driver (break address)
          mov     word ptr es:[di+14],offset Init
          mov     word ptr es:[di+16],cs

          xor     ax,ax           ; return status
          ret

  Init    endp


  hexasc  proc    near            ; converts word to hex ASCII
                                  ; call with AX = value,
                                  ; DS:BX = address for string
                                  ; returns AX, BX destroyed

          push    cx              ; save registers
          push    dx

          mov     dx,4            ; initialize character counter
          mov     cx,4            ; isolate next four bits
          rol     ax,cl
          mov     cx,ax
          and     cx,0fh
          add     cx,'0'          ; convert to ASCII
          cmp     cx,'9'          ; is it 0-9?
          jbe     hexasc2         ; yes, jump
          add     cx,'A'-'9'-1    ; add fudge factor for A-F

  hexasc2:                        ; store this character
          mov     [bx],cl
          inc     bx              ; bump string pointer

          dec     dx              ; count characters converted
          jnz     hexasc1         ; loop, not four yet

          pop     dx              ; restore registers
          pop     cx
          ret                     ; back to caller

  hexasc  endp


  Ident   db      cr,lf,lf
          db      'Advanced MS-DOS Example Device Driver'
          db      cr,lf
          db      'Device driver header at: '
  Ident1  db      'XXXX:0000'
          db      cr,lf,lf,eom

  Intr    endp

  _TEXT   ends

          end
  

  Figure 14-9.  DRIVER.ASM: A functional skeleton from which you can
  implement your own working device driver.

  The driver's device header must be located at the beginning of the file
  (offset 0000H). Both words in the link field in the header should be set
  to -1. The attribute word must be set up correctly for the device type and
  other options. The offsets to the strategy and interrupt routines must be
  relative to the same segment base as the device header itself. If the
  driver is for a character device, the name field should be filled in
  properly with the device's logical name. The logical name can be any legal
  8-character filename, padded with spaces and without a colon. Beware of
  accidentally duplicating the names of existing character devices, unless
  you are intentionally superseding a resident driver.

  MS-DOS calls the strategy and interrupt routines for the device by means
  of an intersegment call (CALL FAR) when the driver is first loaded and
  installed and again whenever an application program issues an I/O request
  for the device. MS-DOS uses the ES:BX registers to pass the strat routine
  a double-word pointer to the request header; this address should be saved
  internally in the driver so that it is available for use during the
  subsequent call to the intr routine.

  The command-code routines for function codes 0 through 12 (0CH) must be
  present in every installable device driver, regardless of device type.
  Functions 13 (0DH) and above are optional for drivers used with MS-DOS
  versions 3.0 and later and can be handled in one of the following ways:

    Don't implement them, and leave the associated bits in the device
     header cleared. The resulting driver will work in either version 2 or
     version 3 but does not take full advantage of the augmented
     functionality of version 3.

    Implement them, and test the MS-DOS version during the initialization
     sequence, setting bits 6 and 11 of the device header appropriately.
     Write all command-code routines so that they test this bit and adjust
     to accommodate the host version of MS-DOS. Such a driver requires more
     work and testing but will take full advantage of both the version 2 and
     the version 3 environments.

    Implement them, and assume that all the version 3 facilities are
     available. With this approach, the resulting driver may not work
     properly under version 2.

  Remember that device drivers must preserve the integrity of MS-DOS. The
  driver must preserve all registers, including flags (especially the
  direction flag and interrupt enable bits), and if the driver makes heavy
  use of the stack, it should switch to an internal stack of adequate depth
  (the MS-DOS stack has room for only 40 to 50 bytes when a driver is
  called).

  If you install a new CON driver, be sure to set the bits for standard
  input and standard output in the device attribute word in the device
  header.

  You'll recall that one file can contain multiple drivers. In this case,
  the device-header link field of each driver should point to the segment
  offset of the next, all using the same segment base, and the link field
  for the last driver in the file should be set to -1,-1. The initialization
  routines for all the drivers in the file should return the same break
  address.

Linking

  Use the standard MS-DOS linker to transform the .OBJ file that is output
  from the assembler into a relocatable .EXE module. Then, use the EXE2BIN
  utility (see Chapter 4) to convert the .EXE file into a memory-image
  program. The extension on the final driver file can be anything, but .BIN
  and .SYS are most commonly used in MS-DOS systems, and it is therefore
  wise to follow one of these conventions.

Installation

  After the driver is assembled, linked, and converted to a .BIN or .SYS
  file, copy it to the root directory of a bootable disk. If it is a
  character-device driver, do not use the same name for the file as you used
  for the logical device listed in the driver's header, or you will not be
  able to delete, copy, or rename the file after the driver is loaded.

  Use your favorite text editor to add the line

    DEVICE=[D:][PATH]FILENAME.EXT

  to the CONFIG.SYS file on the bootable disk. (In this line, D: is an
  optional drive designator and FILENAME.EXT is the name of the file
  containing your new device driver. You can include a path specification in
  the entry if you prefer not to put the driver file in your root
  directory.) Now restart your computer system to load the modified
  CONFIG.SYS file.

  During the MS-DOS boot sequence, the SYSINIT module (which is part of
  IO.SYS) reads and processes the CONFIG.SYS file. It loads the driver into
  memory and inspects the device header. If the driver is a character-device
  driver, SYSINIT links it into the device chain ahead of the other
  character devices; if it is a block-device driver, SYSINIT places it
  behind all previously linked block devices and the resident block devices
  (Figures 14-10, 14-11, and 14-12). It accomplishes the linkage by
  updating the link field in the device header to point to the segment and
  offset of the next driver in the chain. The link field of the last driver
  in the chain contains -1,-1.

  Next, SYSINIT calls the strat routine with a request header that contains
  a command code of zero, and then it calls the intr routine. The driver
  executes its initialization routine and returns the break address, telling
  MS-DOS how much memory to reserve for this driver. Now MS-DOS can proceed
  to the next entry in the CONFIG.SYS file.

  You cannot supersede a built-in block-device driveryou can only add
  supplemental block devices. However, you can override the default system
  driver for a character device (such as CON) with an installed driver by
  giving it the same logical-device name in the device header. When
  processing a character I/O request, MS-DOS always scans the list of
  installed drivers before it scans the list of default devices and takes
  the first match.

             NUL
              |
               
             CON
              |
               
             AUX
              |
               
             PRN
              |
               
            CLOCK
              |
               
  Any other resident block
    or character devices

  Figure 14-10.  MS-DOS device-driver chain before any installable device
  drivers have been loaded.

             NUL
              |
               
   Installable character-
       device drivers
              |
               
             CON
              |
               
             AUX
              |
               
             PRN
              |
               
            CLOCK
              |
               
  Any other resident block
    or character devices
              |
               
     Installable block-
       device drivers

  Figure 14-11.  MS-DOS device-driver chain after installable device drivers
  have been loaded.

  Address          Attribute  Strategy    Interrupt  Type  Units Name
                              routine     routine
  
  00E3:0111        8004       0FD5        0FE0       C           NUL
  0070:0148        8013       008E        0099       C           CON
  0070:01DD        8000       008E        009F       C           AUX
  0070:028E        8000       008E        00AE       C           PRN
  0070:0300        8008       008E        00C3       C           CLOCK
  0070:03CC        0000       008E        00C9       B     02
  0070:01EF        8000       008E        009F       C           COM1
  0070:02A0        8000       008E        00AE       C           LPT1
  0070:06F0        8000       008E        00B4       C           LPT2
  0070:0702        8000       008E        00BA       C           LPT3
  0070:0714        8000       008E        00A5       C           COM2
  End of
  device chain
  

  Figure 14-12.  Example listing of device chain under MS-DOS version 2.1,
  "plain vanilla" IBM PC with no fixed disks or user device drivers.
  (C=character device, B=block device)


Debugging a Device Driver

  The most important thing to remember when testing new device drivers is to
  maintain adequate backups and a viable fallback position. Don't modify the
  CONFIG.SYS file and install the new driver on your fixed disk before it is
  proven! Be prudentcreate a bootable floppy disk and put the modified
  CONFIG.SYS file and the new driver on that for debugging. When everything
  is working properly, copy the finished product to its permanent storage
  medium.

  The easiest way to test a new device driver is to write a simple
  assembly-language front-end routine that sets up a simulated request
  packet and then performs FAR CALLs to the strat and intr entry points,
  exactly as MS-DOS would. You can then link the driver and the front end
  together into a .COM or .EXE file that can be run under the control of
  CodeView or another debugger. This arrangement makes it easy to trace each
  of the command-code routines individually, to observe the results of the
  I/O, and to examine the status codes returned in the request header.

  Tracing the installed driver when it is linked into the MS-DOS system in
  the normal manner is more difficult. Breakpoints must be chosen carefully,
  to yield the maximum possible information per debugging run. Because
  current versions of MS-DOS maintain only one request header internally,
  the request header that was being used by the driver you are tracing will
  be overwritten as soon as your debugger makes an output request to display
  information. You will find it helpful to add a routine to your
  initialization subroutine that displays the driver's load address on the
  console when you boot MS-DOS; you can then use this address to inspect the
  device-driver header and set breakpoints within the body of the driver.

  Debugging a device driver can also be somewhat sticky when interrupt
  handling is involved, especially if the device uses the same
  interrupt-request priority level (IRQ level) as other peripherals in the
  system. Cautious, conservative programming is needed to avoid unexpected
  and unreproducible interactions with other device drivers and interrupt
  handlers. If possible, prove out the basic logic of the driver using
  polled I/O, rather than interrupt-driven I/O, and introduce interrupt
  handling only when you know the rest of the driver's logic to be solid.

  Typical device-driver errors or problems that can cause system crashes or
  strange system behavior include the following:

    Failure to set the linkage address of the last driver in a file to -1

    Overflow of the MS-DOS stack by driver-initialization code, corrupting
     the memory image of MS-DOS (can lead to unpredictable behavior during
     boot; remedy is to use a local stack)

    Incorrect break-address reporting by the initialization routine (can
     lead to a system crash if the next driver loaded overwrites vital parts
     of the driver)

    Improper BPBs supplied by the build BPB routine, or incorrect BPB
     pointer array supplied by the initialization routine (can lead to many
     confusing problems, ranging from out-of-memory errors to system boot
     failure)

    Incorrect reporting of the number of bytes or sectors successfully
     transferred at the time an I/O error occurs (can manifest itself as a
     system crash after you enter R to the Abort, Retry, Ignore? prompt)

  Although the interface between the DOS kernel and the device driver is
  fairly simple, it is also quite strict. The command-code routines must
  perform exactly as they are defined, or the system will behave
  erratically. Even a very subtle discrepancy in the action of a
  command-code routine can have unexpectedly large global effects.



